using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightToolbox
{
    public class DragAndDropOverlayExtender : ControlExtenderBase
    {
        public DragAndDropOverlayExtender()
        {
            this.BaseLoaded += new EventHandler(DragAndDropOverlayExtender_Loaded);
        }

        void DragAndDropOverlayExtender_Loaded(object sender, EventArgs e)
        {
            if (!Disabled)
                AttachEventHandlersToElementToMove();
        }

        private void AttachEventHandlersToElementToMove()
        {
            foreach (FrameworkElement elementToMove in ElementsToMove)
            {
                AttacheToMoveLeftButtonDown_MouseLeftButtonUp_MouseMove(elementToMove);
                if (allowedAreas.Count != 0)
                    AttachMoveLeftButtonDownOneClickMoveToAllowedArea();
                GetRootCanvaS(this).MouseLeave += new EventHandler(DragAndDropOverlayExtender_MouseLeave);
            }
        }

        //private FrameworkElement elementToMove;

        private List<FrameworkElement> _ElementsToMove = new List<FrameworkElement>();
        [FillXamlElementssByName("ElementsToMoveName")]
        internal List<FrameworkElement> ElementsToMove
        {
            get
            { return _ElementsToMove; }
            set { _ElementsToMove = value; }
        }


        void DragAndDropOverlayExtender_MouseLeave(object sender, EventArgs e)
        {
            if (_isMouseDown)
                EndDragAndDrop(null);
        }

        private void AttachMoveLeftButtonDownOneClickMoveToAllowedArea()
        {
            foreach (FrameworkElement curAllowedArea in allowedAreas)
            {
                curAllowedArea.MouseLeftButtonDown += new MouseEventHandler(allowedArea_MouseLeftButtonDown);
            }
        }


        void allowedArea_MouseLeftButtonDown(object sender, MouseEventArgs e)
        {
            if (EnableOneClickMove)
            {
                foreach (FrameworkElement elementToMove in ElementsToMove)
                {
                    MoveLeftAndTopToNewLocationIfConditionasMet(e, elementToMove);
                }
                StartDragAndDrop(ElementsToMove[ElementsToMove.Count - 1]);
            }
        }

        private void AttacheToMoveLeftButtonDown_MouseLeftButtonUp_MouseMove(UIElement elementToMove)
        {
            elementToMove.MouseLeftButtonDown += new MouseEventHandler(elementToMove_MouseLeftButtonDown);
            elementToMove.MouseLeftButtonUp += new MouseEventHandler(elementToMove_MouseLeftButtonUp);
            elementToMove.MouseMove += new MouseEventHandler(elementToMove_MouseMove);
        }


        private bool _isMouseDown = false;
        void elementToMove_MouseMove(object sender, MouseEventArgs e)
        {
            if (!IsMouseInParentCanvas(e))
            {
                EndDragAndDrop(e);
            }
            if (_isMouseDown)
            {
                MoveLeftAndTopToNewLocationIfConditionasMet(e, sender as FrameworkElement);
            }
        }

        private void MoveLeftAndTopToNewLocationIfConditionasMet(MouseEventArgs e, FrameworkElement elementToMove)
        {
            Point currentCurserLocationRelativeToElementToMove = e.GetPosition(elementToMove);

            FrameworkElement hasMovedTo = null;
            if (IsMoveLeft)
            {
                if (this.IsSingleAreaDragAndDrop)
                    hasMovedTo = CheckIfCanMoveElementXToAreaAndIfSoDoIt(e, allowedArea, elementToMove);
                else
                {
                    MoveLeft(e.GetPosition(elementToMove).X, elementToMove);
                    hasMovedTo = allowedArea;
                }
            }


            if (IsMoveTop)
            {
                if (this.IsSingleAreaDragAndDrop)
                    hasMovedTo = CheckIfCanMoveElementYToAreaAndIfSoDoIt(e, allowedArea, elementToMove);
                else
                {
                    MoveTop(e.GetPosition(elementToMove).Y, elementToMove);
                    hasMovedTo = allowedArea;
                }
            }

            if (hasMovedTo != null)
            {
                RaiseElementMoved(new ElementMovedEventArgs(elementToMove, hasMovedTo));
            }
        }

        private FrameworkElement CheckIfCanMoveElementXToAreaAndIfSoDoIt(MouseEventArgs e, FrameworkElement curAllowedAreaToCheckIfCanMove, FrameworkElement elementToMove)
        {
            FrameworkElement hasMovedTo = null;
            double currentCurserLocationRelativeToElementToMoveX =
                GetXInBoundsOrOutMoustBounds_BasedOnIntersectionWithMouse(curAllowedAreaToCheckIfCanMove, e, elementToMove);
            if (currentCurserLocationRelativeToElementToMoveX != 0)
            {
                MoveLeft(currentCurserLocationRelativeToElementToMoveX, elementToMove);
                hasMovedTo = curAllowedAreaToCheckIfCanMove;
            }
            return hasMovedTo;
        }

        private FrameworkElement CheckIfCanMoveElementYToAreaAndIfSoDoIt(MouseEventArgs e, FrameworkElement curAllowedAreaToCheckIfCanMove, FrameworkElement elementToMove)
        {
            FrameworkElement hasMovedTo = null;
            double currentCurserLocationRelativeToElementToMoveY =
                GetYInBoundsOrOutMoustBounds_BasedOnIntersectionWithMouse(curAllowedAreaToCheckIfCanMove, e, elementToMove);
            if (currentCurserLocationRelativeToElementToMoveY != 0)
            {
                MoveTop(currentCurserLocationRelativeToElementToMoveY, elementToMove);
                hasMovedTo = curAllowedAreaToCheckIfCanMove;
            }
            return hasMovedTo;
        }

        private void RaiseElementMoved(ElementMovedEventArgs e)
        {
            if (ElementMoved != null)
                ElementMoved(this, e);
        }

        private void RaiseDrop(ElementMovedEventArgs e)
        {
            if (Drop != null)
                Drop(this, e);
        }

        private void RaiseDrag(ElementMovedEventArgs e)
        {
            if (Drag != null)
                Drag(this, e);
        }

        public event EventHandler<ElementMovedEventArgs> ElementMoved;
        public event EventHandler<ElementMovedEventArgs> Drag;
        public event EventHandler<ElementMovedEventArgs> Drop;

        private List<FrameworkElement> _AllowedAreasElement = new List<FrameworkElement>();
        [FillXamlElementssByName("AllowedAreasElementName")]
        internal List<FrameworkElement> allowedAreas
        {
            get
            {
                return _AllowedAreasElement;
            }
        }

        private FrameworkElement allowedArea
        {
            get
            {
                if (allowedAreas.Count != 0)
                    return allowedAreas[0];
                return null;
            }
        }

        private double halfElementToMoveHeight(FrameworkElement elementToMove)
        {

            return (elementToMove.Height / 2) - 1;

        }

        private void MoveTop(double currentCurserLocationRelativeToElementToMoveY, FrameworkElement elementToMove)
        {
            double curAbosuluteYPoistion = (double)elementToMove.GetValue(Canvas.TopProperty);
            double newAbosuluteYPoistion = curAbosuluteYPoistion + currentCurserLocationRelativeToElementToMoveY -
                                           halfElementToMoveHeight(elementToMove);
            elementToMove.SetValue(Canvas.TopProperty, newAbosuluteYPoistion);

        }

        private double halfElementToMoveWidth(FrameworkElement elementToMove)
        {
            return (elementToMove.Width / 2) - 1;

        }

        private void MoveLeft(double currentCurserLocationRelativeToElementToMoveX, FrameworkElement elementToMove)
        {

            double curAbosuluteXPoistion = (double)elementToMove.GetValue(Canvas.LeftProperty);
            double newAbosuluteXPoistion = curAbosuluteXPoistion + currentCurserLocationRelativeToElementToMoveX -
                                           halfElementToMoveWidth(elementToMove);
            elementToMove.SetValue(Canvas.LeftProperty, newAbosuluteXPoistion);
        }

        private bool IsMoveTop
        {
            get
            {
                return !DoNotChangeCanvasTop;
            }
        }

        private bool IsMoveLeft
        {
            get
            {
                return !DoNotChangeCanvasLeft;
            }
        }


        void elementToMove_MouseLeftButtonUp(object sender, MouseEventArgs e)
        {
            EndDragAndDrop(e);
        }

        private void EndDragAndDrop(MouseEventArgs e)
        {
            if (IsMultipleAreasDragAndDrop)
            {
                CheckIfIsOnOneSelectedAreaAtLeastOrSnapBackToLocationBeforeDrag(e, _lastElementToMove);
            }
            _lastElementToMove.ReleaseMouseCapture();
            _lastElementToMove.Opacity = _opacityBeforeDragging;
            SetAllElementParentHierchyToZIndexOf(_lastElementToMove, _ZIndexBeforeDragging);
            //_lastElementToMove.SetValue<int>(Canvas.ZIndexProperty, _ZIndexBeforeDragging);
            //IterateOverCanvasAndSetZIndexToZIndex(GetRootCanvaS(this));
            _isMouseDown = false;
            RaiseDrop(new ElementMovedEventArgs(_lastElementToMove, null));
        }

        private void IterateOverCanvasAndSetZIndexToZIndex(Canvas canvasToIterateOver)
        {
            foreach (Visual curChildToSetItsZIndexOrIterateOverChildren in canvasToIterateOver.Children)
            {
                curChildToSetItsZIndexOrIterateOverChildren.SetValue(Canvas.ZIndexProperty, (int)curChildToSetItsZIndexOrIterateOverChildren.GetValue(Canvas.ZIndexProperty) - 1);
                if (curChildToSetItsZIndexOrIterateOverChildren is Canvas)
                {
                    IterateOverCanvasAndSetZIndexToZIndex((Canvas)curChildToSetItsZIndexOrIterateOverChildren);
                }
            }
        }

        private void CheckIfIsOnOneSelectedAreaAtLeastOrSnapBackToLocationBeforeDrag(MouseEventArgs e, FrameworkElement elementToMove)
        {
            bool inAtLeastOneAllowedArea = false;
            foreach (FrameworkElement curAllowedAreaToCheckIfElementIsInside in allowedAreas)
            {
                if (GetXInBoundsOrOutMoustBounds_BasedOnIntersectionWithMouse(curAllowedAreaToCheckIfElementIsInside, e, elementToMove) != 0
                    && GetYInBoundsOrOutMoustBounds_BasedOnIntersectionWithMouse(curAllowedAreaToCheckIfElementIsInside, e, elementToMove) != 0)
                    inAtLeastOneAllowedArea = true;
            }
            if (!inAtLeastOneAllowedArea)
                SnapBackToOriginalLocation();
        }

        private void SnapBackToOriginalLocation()
        {
            _lastElementToMove.SetValue(Canvas.TopProperty, _CanvasTopBeforeDragging);
            _lastElementToMove.SetValue(Canvas.LeftProperty, _CanvasLeftBeforeDragging);
        }

        private bool IsMultipleAreasDragAndDrop
        {
            get
            {
                return allowedAreas.Count > 1;
            }
        }

        private bool IsSingleAreaDragAndDrop
        {
            get
            {
                return allowedAreas.Count == 1;
            }
        }

        private double _opacityBeforeDragging;
        private FrameworkElement _lastElementToMove;
        private void StartDragAndDrop(FrameworkElement elementToMove)
        {
            if (!Disabled)
            {
                elementToMove.CaptureMouse();
                _opacityBeforeDragging = elementToMove.Opacity;
                elementToMove.Opacity = InternalDraggingOpacity;
                _CanvasLeftBeforeDragging = (double)elementToMove.GetValue(Canvas.LeftProperty);
                _CanvasTopBeforeDragging = (double)elementToMove.GetValue(Canvas.TopProperty);
                _lastElementToMove = elementToMove;
                _ZIndexBeforeDragging = (int)elementToMove.GetValue(Canvas.ZIndexProperty);
                SetAllElementParentHierchyToZIndexOf(elementToMove, Int16.MaxValue - 1);

                _isMouseDown = true;
                RaiseDrag(new ElementMovedEventArgs(elementToMove, null));
            }
        }

        private void SetAllElementParentHierchyToZIndexOf(FrameworkElement elementToMove, int newZIndex)
        {
            elementToMove.SetValue(Canvas.ZIndexProperty, newZIndex);
            if (elementToMove.Parent != null)
                SetAllElementParentHierchyToZIndexOf((FrameworkElement)elementToMove.Parent, newZIndex);
        }

        void elementToMove_MouseLeftButtonDown(object sender, MouseEventArgs e)
        {
            StartDragAndDrop(sender as FrameworkElement);
        }

        public bool IsMouseInParentCanvas(MouseEventArgs e)
        {
            Canvas cnvs = GetRootCanvaS(this);

            // Determine where the X/Y Mouse is within the Canvas
            double cnvsX = e.GetPosition(cnvs).X;
            double cnvsY = e.GetPosition(cnvs).Y;


            // IsMouseInsideCanvas ?
            return ((cnvsX >= 0 && cnvsX <= cnvs.Width)
                    && (cnvsY >= 0 && cnvsY <= cnvs.Height));


        }
        public double GetXInBoundsOrOutMoustBounds_BasedOnIntersectionWithMouse(FrameworkElement area, MouseEventArgs e, FrameworkElement elementToMove)
        {
            if (e == null)
                return 0;
            // Determine where the X/Y Mouse is within the Rectangle
            double areaX = e.GetPosition(area).X;
            double elementToMoveRelativeX = e.GetPosition(elementToMove).X;
            double halfElementToMoveWidth = this.halfElementToMoveWidth(elementToMove);

            if ((area == null) ||
                ((areaX >= 0 + halfElementToMoveWidth)
                 && (areaX <= area.Width - halfElementToMoveWidth)))
                return elementToMoveRelativeX;

            if (this.IsMultipleAreasDragAndDrop)
                return 0;
            else
                if (areaX < 0 + halfElementToMoveWidth)
                    return elementToMoveRelativeX - areaX + halfElementToMoveWidth;
                else if (areaX > area.Width - halfElementToMoveWidth)
                    return elementToMoveRelativeX - (areaX - area.Width) - halfElementToMoveWidth; //(area.Width + (double)area.GetValue(Canvas.LeftProperty) - (double)elementToMove.GetValue(Canvas.LeftProperty)); 
                else
                    return 0;
        }

        public double GetYInBoundsOrOutMoustBounds_BasedOnIntersectionWithMouse(FrameworkElement area, MouseEventArgs e, FrameworkElement elementToMove)
        {
            if (e == null)
                return 0;
            // Determine where the X/Y Mouse is within the Rectangle
            double areaY = e.GetPosition(area).Y;
            double elementToMoveRelativeY = e.GetPosition(elementToMove).Y;
            double halfElementToMoveHeight = this.halfElementToMoveHeight(elementToMove);

            if (area == null)
                return elementToMoveRelativeY;

            if ((areaY >= 0 + halfElementToMoveHeight)
                && (areaY <= area.Height - halfElementToMoveHeight))

                return elementToMoveRelativeY;
            //if ((areaY >= 0)
            //              && (areaY <= area.Height - halfElementToMoveHeight))
            //    return elementToMoveRelativeY - halfElementToMoveHeight;
            //if ((areaY >= 0 + halfElementToMoveHeight)
            //  && (areaY <= area.Height))
            //    return elementToMoveRelativeY + halfElementToMoveHeight - areaY;





            if (this.IsMultipleAreasDragAndDrop)
                return 0;
            else
                if (areaY < 0 + halfElementToMoveHeight)
                    return elementToMoveRelativeY - areaY + halfElementToMoveHeight;
                else if (areaY > area.Height - halfElementToMoveHeight)
                    return elementToMoveRelativeY - (areaY - area.Height) - halfElementToMoveHeight;
                else
                    return 0;
        }




        private static Canvas GetRootCanvaS(FrameworkElement elementToCheckIfHasNoParentThenIsRoot)
        {
            if (elementToCheckIfHasNoParentThenIsRoot.Parent == null)
                return (Canvas)elementToCheckIfHasNoParentThenIsRoot;
            else
                return GetRootCanvaS((FrameworkElement)elementToCheckIfHasNoParentThenIsRoot.Parent);
        }

        public string ElementsToMoveName
        {
            get;
            set;
        }

        public string AllowedAreasElementName
        {
            get;
            set;
        }

        public bool DoNotChangeCanvasLeft
        {
            get;
            set;
        }


        public bool DoNotChangeCanvasTop
        {
            get;
            set;
        }

        public bool EnableOneClickMove
        {
            get;
            set;
        }

        private double _draggingOpacty = 0.4;
        private double _CanvasLeftBeforeDragging;
        private double _CanvasTopBeforeDragging;
        private int _ZIndexBeforeDragging;

        private double InternalDraggingOpacity
        {
            get
            {
                if (!string.IsNullOrEmpty(DraggingOpacity))
                    _draggingOpacty = Convert.ToDouble(DraggingOpacity);
                return _draggingOpacty;
            }
            set
            {
                _draggingOpacty = value;
            }
        }

        public string DraggingOpacity
        {
            get;
            set;
        }



        public bool Disabled
        {
            get;
            set;
        }
    }

    public class ElementMovedEventArgs : EventArgs
    {
        public ElementMovedEventArgs(FrameworkElement Element, FrameworkElement AllowedArea)
        {
            this.Element = Element;
            this.AllowedArea = AllowedArea;

        }

        public FrameworkElement Element
        {
            get;
            set;
        }

        public FrameworkElement AllowedArea
        {
            get;
            set;
        }

        public double CanvasLeftPercent
        {
            get
            {
                if (AllowedArea == null)
                    return 0;
                return
                    Math.Round((((double)Element.GetValue(Canvas.LeftProperty) -
                                 (double)AllowedArea.GetValue(Canvas.LeftProperty)) /
                                (AllowedArea.Width - Element.Width / 2 - 1)) * 1, 2);
            }
        }

        public double CanvasTopPercent
        {
            get
            {
                if (AllowedArea == null)
                    return 0;
                return Math.Round((((double)Element.GetValue(Canvas.TopProperty) -
                                    (double)AllowedArea.GetValue(Canvas.TopProperty)) /
                                   (AllowedArea.Height - Element.Height / 2 - 1)) * 1, 2);
            }
        }
    }
}